---
title: Guide Documents > E2E Testing > Why E2E Test?
---
import { Alert, Tabs } from 'nextra/components'

## Outline
E2E test functions rather than unit test functions.

When developing a test program for NestJS developed backend server, I recommend to adapt E2E test paradigm instead of unit test paradigm. It's because with the [`@nestia/sdk` generated SDK library](../sdk/sdk), E2E test functions can be much easier, safer and efficient for production than the traditional unit test functions.

Furthermore, if you develop test functions utilizing the SDK library, you can easily switch the e2e test functions to the performance benchmark functions. Just by utilizing `@nestia/e2e` and `@nestia/benchmark` libraries, you can easily measure your NestJS developed backend server's performance through the SDK library utilizing e2e test functions.

  - [E2E Test Function Example](https://github.com/samchon/nestia-start/blob/master/test/features/api/bbs/test_api_bbs_article_create.ts)
  - [Benchmark Result Example](https://github.com/samchon/nestia-start/blob/master/docs/benchmarks/AMD%20Ryzen%209%207940HS%20w%20Radeon%20780M%20Graphics.md)

![SDK](https://user-images.githubusercontent.com/13158709/215004990-368c589d-7101-404e-b81b-fbc936382f05.gif)

> SDK library utilizing test functions can be used in the performance benchmark program




## Efficient for Production
New era, age of E2E testing paradigm comes.

In the past era, backend developers had developed test programs following the unit test paradigm. It's because traditional E2E test functions' development could not take any advantage of compiled language's type safety. As you can see from the below example code, E2E test functions had to write hard-coded `fetch()` function with string literals.

Besides, unit test could take advantages of compiled language's type safety by importing related modules. Therefore, it was more efficient to develop test functions following the unit test paradigm. This is the reason why unit test paradigm had been loved in the past era.

<Tabs items={[
    "Traditional E2E Test",
    "Traditional Unit Test"
  ]}>
  <Tabs.Tab>
```typescript showLineNumbers
import { TestValidator } from "@nestia/e2e";
import typia from "typia";

import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle";

export const test_api_bbs_article_create = async (
  host: string,
): Promise<void> => {
  // In the traditional age, E2E test function could not take advantages 
  // of type safety of the TypeScript. Instead, have to write hard-coded
  // `fetch()` function with string literals.
  const response: Response = await fetch(`${host}/bbs/articles`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      writer: "someone",
      password: "1234",
      title: "title",
      body: "content",
      format: "md",
      files: [],
    } satisfies IBbsArticle.ICreate),
  });
  const article: IBbsArticle = await response.json();
  typia.assert(article);

  const read: IBbsArticle = await (async () => {
    const response: Response = await fetch(`${host}/bbs/articles/${article.id}`);
    const article: IBbsArticle = await response.json();
    return typia.assert(article);
  })();
  TestValidator.equals("written")(article)(read);
};
```
  </Tabs.Tab>
  <Tabs.Tab>
```typescript showLineNumbers
import { TestValidator } from "@nestia/e2e";
import typia from "typia";

import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle";

import { BbsArticleProvider } from "../../../../src/providers/bbs/BbsArticleProvider";

export const test_provider_bbs_article_create = async (): Promise<void> => {
  // Besides, unit test could take advantages of type safety.
  const article: IBbsArticle = await BbsArticleProvider.write({
    writer: "someone",
    password: "1234",
    title: "title",
    body: "content",
    format: "md",
    files: [],
  });
  typia.assert(article);

  // Therefore, unit test had been loved in the past era
  const read: IBbsArticle = await BbsArticleProvider.at(article.id);
  typia.assert(read);
  TestValidator.equals("written")(article)(read);
};
```
  </Tabs.Tab>
</Tabs>

However, in the new era, e2e test functions also can take advantages of the type safety. Just import SDK library generated by `@nestia/e2e`, and call API functions of it with TypeScript type hints. In this way, as both e2e test paradigm and unit test paradigm can take advantages of type safety, we have to consider which strategy is suitable for the production environment.

In here article, I recommend to adapt e2e test paradigm in the below reasons.

  - suitable for CDD (Contract Driven Development)
  - easy to develop and maintain
  - much safer than unit testing due to its coverage
  - can be used for the performance benchmark
  - can guide client developers as an example code

Look at the below example code of new era's e2e test function, and compare it with the traditional unit test function. As you can see, e2e test function actually tests the backend server's behavior by calling the real API endpoints of it. Besides, unit test can't test the backend server's actual behavior. It just validates the provider's behavior. This is the reason why new era's e2e test function is safer than unit test function.

Also, such new era's e2e test function can be provided to the client or frontend developers as an example code. It guides them how to call the backend server's API endpoints through the SDK library. In this way, e2e test function can be used as a well-structured document for the client developers.

<Tabs items={[
    "New era's E2E Test",
    "Traditional Unit Test"
  ]}>
  <Tabs.Tab>
```typescript showLineNumbers
import { TestValidator } from "@nestia/e2e";
import typia from "typia";

import BbsApi from "@samchon/bbs-api";
import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle";

export const test_provider_bbs_article_create = async (
  connection: BbsApi.IConnection
): Promise<void> => {
  // Unit test functions can't validate 
  // the backend server's actual behavior.
  const article: IBbsArticle = await BbsApi.functional.bbs.articles.create(
    connection,
    {
      writer: "someone",
      password: "1234",
      title: "title",
      body: "content",
      format: "md",
      files: [],
    },
  );
  typia.assert(article);

  // This is the reason why I've adopted the e2e test paradigm
  const read: IBbsArticle = await BbsApi.functional.bbs.articles.at(
    connection,
    article.id,
  );
  typia.assert(read);
  TestValidator.equals("written")(article)(read);
};
```
  </Tabs.Tab>
  <Tabs.Tab>
```typescript showLineNumbers
import { TestValidator } from "@nestia/e2e";
import typia from "typia";

import BbsApi from "@samchon/bbs-api";

import { BbsArticleProvider } from "../../../../src/providers/bbs/BbsArticleProvider";

export const test_provider_bbs_article_create = async (): Promise<void> => {
  // Unit test functions can't validate 
  // the backend server's actual behavior.
  const article: IBbsArticle = await BbsArticleProvider.write({
    writer: "someone",
    password: "1234",
    title: "title",
    body: "content",
    format: "md",
    files: [],
  });
  typia.assert(article);

  // This is the reason why I've adopted the e2e test paradigm
  const read: IBbsArticle = await BbsArticleProvider.at(article.id);
  typia.assert(read);
  TestValidator.equals("written")(article)(read);
};
```
  </Tabs.Tab>
</Tabs>

At last, the new era's e2e test functions can be used for the performance benchmark without any extra dedication. As the e2e test function directly calls the API endpoints of the backend server, backend server performance benchmark program can be easily developed by utilizing them.

Those are the reasons why I recommend to adapt the new era's e2e test paradigm instead of the traditional unit test paradigm. Those are the reason why I am insisting that e2e test functions are efficient for production. From now on, let's see how to compose the e2e test functions for the NestJS developed backend server.

> You can experience benchmark program utilizing e2e test functions.
>
> 💻 [https://stackblitz.com/~/github.com/samchon/nestia-start](https://stackblitz.com/~/github.com/samchon/nestia-start?file=README.md,docs/benchmarks/AMD%20Ryzen%209%207940HS%20w%20Radeon%20780M%20Graphics.md!,test/benchmark/index.ts,test/benchmark/servant.ts,test/features/api/bbs/test_api_bbs_article_create.ts&startScript=build:test,benchmark&view=editor)
>
> [![Benchmark Result Screenshot](/images/benchmark-dark.png)](https://stackblitz.com/~/github.com/samchon/nestia-start?file=README.md,docs/benchmarks/AMD%20Ryzen%209%207940HS%20w%20Radeon%20780M%20Graphics.md!,test/benchmark/index.ts,test/benchmark/servant.ts,test/features/api/bbs/test_api_bbs_article_create.ts&startScript=build:test,benchmark&view=editor)




## Test Program Development
<Tabs items={[
    "Test Main Program",
    "Test Function #1",
    "Test Function #2",
  ]}
  selectedIndex={1}>
  <Tabs.Tab>
```typescript filename="test/index.ts" showLineNumbers {51-59}
import { DynamicExecutor } from "@nestia/e2e";

import api from "@ORGANIZATION/PROJECT-api";

import { MyBackend } from "../src/MyBackend";
import { MyConfiguration } from "../src/MyConfiguration";
import { MyGlobal } from "../src/MyGlobal";
import { ArgumentParser } from "./helpers/ArgumentParser";

interface IOptions {
  include?: string[];
  exclude?: string[];
}

const getOptions = () =>
  ArgumentParser.parse<IOptions>(async (command, _prompt, action) => {
    // command.option("--mode <string>", "target mode");
    // command.option("--reset <true|false>", "reset local DB or not");
    command.option("--include <string...>", "include feature files");
    command.option("--exclude <string...>", "exclude feature files");

    return action(async (options) => {
      // if (typeof options.reset === "string")
      //     options.reset = options.reset === "true";
      // options.mode ??= await prompt.select("mode")("Select mode")([
      //     "LOCAL",
      //     "DEV",
      //     "REAL",
      // ]);
      // options.reset ??= await prompt.boolean("reset")("Reset local DB");
      return options as IOptions;
    });
  });

async function main(): Promise<void> {
  // CONFIGURATIONS
  const options: IOptions = await getOptions();
  MyGlobal.testing = true;

  // BACKEND SERVER
  const backend: MyBackend = new MyBackend();
  await backend.open();

  //----
  // CLIENT CONNECTOR
  //----
  // DO TEST
  const connection: api.IConnection = {
    host: `http://127.0.0.1:${MyConfiguration.API_PORT()}`,
  };
  const report: DynamicExecutor.IReport = await DynamicExecutor.validate({
    prefix: "test",
    parameters: () => [{ ...connection }],
    filter: (func) =>
      (!options.include?.length ||
        (options.include ?? []).some((str) => func.includes(str))) &&
      (!options.exclude?.length ||
        (options.exclude ?? []).every((str) => !func.includes(str))),
  })(__dirname + "/features");

  await backend.close();

  const failures: DynamicExecutor.IReport.IExecution[] =
    report.executions.filter((exec) => exec.error !== null);
  if (failures.length === 0) {
    console.log("Success");
    console.log("Elapsed time", report.time.toLocaleString(), `ms`);
  } else {
    for (const f of failures) console.log(f.error);
    process.exit(-1);
  }

  console.log(
    [
      `All: #${report.executions.length}`,
      `Success: #${report.executions.length - failures.length}`,
      `Failed: #${failures.length}`,
    ].join("\n"),
  );
}
main().catch((exp) => {
  console.log(exp);
  process.exit(-1);
});
```
  </Tabs.Tab>
  <Tabs.Tab>
```typescript filename="test/features/api/bbs/test_api_bbs_article_create.ts" showLineNumbers
import { RandomGenerator, TestValidator } from "@nestia/e2e";
import { v4 } from "uuid";

import api from "@ORGANIZATION/PROJECT-api/lib/index";
import { IBbsArticle } from "@ORGANIZATION/PROJECT-api/lib/structures/bbs/IBbsArticle";

export async function test_api_bbs_article_create(
  connection: api.IConnection,
): Promise<void> {
  // STORE A NEW ARTICLE
  const stored: IBbsArticle = await api.functional.bbs.articles.create(
    connection,
    "general",
    {
      writer: RandomGenerator.name(),
      title: RandomGenerator.paragraph(3)(),
      body: RandomGenerator.content(8)()(),
      format: "txt",
      files: [
        {
          name: "logo",
          extension: "png",
          url: "https://somewhere.com/logo.png",
        },
      ],
      password: v4(),
    },
  );

  // READ THE DATA AGAIN
  const read: IBbsArticle = await api.functional.bbs.articles.at(
    connection,
    stored.section,
    stored.id,
  );
  TestValidator.equals("created")(stored)(read);
}
```
  </Tabs.Tab>
  <Tabs.Tab>
```typescript filename="test/features/api/bbs/test_api_bbs_article_index_search.ts" showLineNumbers
import { ArrayUtil, RandomGenerator, TestValidator } from "@nestia/e2e";

import api from "@ORGANIZATION/PROJECT-api/lib/index";
import { IBbsArticle } from "@ORGANIZATION/PROJECT-api/lib/structures/bbs/IBbsArticle";
import { IPage } from "@ORGANIZATION/PROJECT-api/lib/structures/common/IPage";

export async function test_api_bbs_article_index_search(
  connection: api.IConnection,
): Promise<void> {
  // GENERATE 100 ARTICLES
  const section: string = "general";
  const articles: IBbsArticle[] = await ArrayUtil.asyncRepeat(100)(() =>
    api.functional.bbs.articles.create(connection, section, {
      writer: RandomGenerator.name(),
      title: RandomGenerator.paragraph(4)(),
      body: RandomGenerator.content(3)()(),
      format: "txt",
      files: [],
      password: RandomGenerator.alphabets(8),
    }),
  );

  // GET ENTIRE DATA
  const total: IPage<IBbsArticle.ISummary> =
    await api.functional.bbs.articles.index(connection, section, {
      limit: articles.length,
      sort: ["-created_at"],
    });

  // PREPARE SEARCH FUNCTION
  const search = TestValidator.search("BbsArticleProvider.index()")(
    async (input: IBbsArticle.IRequest.ISearch) => {
      const page: IPage<IBbsArticle.ISummary> =
        await api.functional.bbs.articles.index(connection, section, {
          limit: articles.length,
          search: input,
          sort: ["-created_at"],
        });
      return page.data;
    },
  )(total.data, 10);

  // SEARCH TITLE
  await search({
    fields: ["title"],
    values: (article) => [article.title],
    request: ([title]) => ({ title }),
    filter: (article, [title]) => article.title.includes(title),
  });

  // SEARCH WRITER
  await search({
    fields: ["writer"],
    values: (article) => [article.writer],
    request: ([writer]) => ({ writer }),
    filter: (article, [writer]) => article.writer.includes(writer),
  });

  // SEARCH BOTH OF THEM
  await search({
    fields: ["title", "writer"],
    values: (article) => [article.title, article.writer],
    request: ([title, writer]) => ({ title, writer }),
    filter: (article, [title, writer]) =>
      article.title.includes(title) && article.writer.includes(writer),
  });
}
```
  </Tabs.Tab>
</Tabs>

Just make test functions utilizing SDK library, and export those functions with `test_` prefixed names. And then if compose the main program of the test, all of the test functions would be automatically mounted and executed whenever you run the main test program.

If you want to experience the test program earlier, visit below playground website.

💻 [https://stackblitz.com/~/github.com/samchon/nestia-start](https://stackblitz.com/~/github.com/samchon/nestia-start?file=README.md,test/index.ts,test/features/api/bbs/test_api_bbs_article_create.ts!&view=editor)

> ```bash
> - test_api_bbs_article_at: 149 ms
> - test_api_bbs_article_create: 30 ms
> - test_api_bbs_article_index_search: 1,312 ms
> - test_api_bbs_article_index_sort: 1,110 ms
> - test_api_bbs_article_update: 28 m
> ```


## Performance Benchmark
<Tabs items={[
    "Benchmark Main Program",
    "Benchmark Servant Program",
    "Test Function #1",
    "Test Function #2",
  ]}
  selectedIndex={1}>
  <Tabs.Tab>
```typescript filename="test/benchmark/index.ts" showLineNumbers {76-94}
import { DynamicBenchmarker } from "@nestia/benchmark";
import cliProgress from "cli-progress";
import fs from "fs";
import os from "os";
import { IPointer } from "tstl";

import { MyBackend } from "../../src/MyBackend";
import { MyConfiguration } from "../../src/MyConfiguration";
import { MyGlobal } from "../../src/MyGlobal";
import { ArgumentParser } from "../helpers/ArgumentParser";

interface IOptions {
  include?: string[];
  exclude?: string[];
  count: number;
  threads: number;
  simultaneous: number;
}

const getOptions = () =>
  ArgumentParser.parse<IOptions>(async (command, prompt, action) => {
    // command.option("--mode <string>", "target mode");
    // command.option("--reset <true|false>", "reset local DB or not");
    command.option("--include <string...>", "include feature files");
    command.option("--exclude <string...>", "exclude feature files");
    command.option("--count <number>", "number of requests to make");
    command.option("--threads <number>", "number of threads to use");
    command.option(
      "--simultaneous <number>",
      "number of simultaneous requests to make",
    );
    return action(async (options) => {
      // if (typeof options.reset === "string")
      //     options.reset = options.reset === "true";
      // options.mode ??= await prompt.select("mode")("Select mode")([
      //     "LOCAL",
      //     "DEV",
      //     "REAL",
      // ]);
      // options.reset ??= await prompt.boolean("reset")("Reset local DB");
      options.count = Number(
        options.count ??
          (await prompt.number("count")("Number of requests to make")),
      );
      options.threads = Number(
        options.threads ??
          (await prompt.number("threads")("Number of threads to use")),
      );
      options.simultaneous = Number(
        options.simultaneous ??
          (await prompt.number("simultaneous")(
            "Number of simultaneous requests to make",
          )),
      );
      return options as IOptions;
    });
  });

const main = async (): Promise<void> => {
  // CONFIGURATIONS
  const options: IOptions = await getOptions();
  MyGlobal.testing = true;

  // BACKEND SERVER
  const backend: MyBackend = new MyBackend();
  await backend.open();

  // DO BENCHMARK
  const prev: IPointer<number> = { value: 0 };
  const bar: cliProgress.SingleBar = new cliProgress.SingleBar(
    {},
    cliProgress.Presets.shades_classic,
  );
  bar.start(options.count, 0);

  const report: DynamicBenchmarker.IReport = await DynamicBenchmarker.master({
    servant: `${__dirname}/servant.js`,
    count: options.count,
    threads: options.threads,
    simultaneous: options.simultaneous,
    filter: (func) =>
      (!options.include?.length ||
        (options.include ?? []).some((str) => func.includes(str))) &&
      (!options.exclude?.length ||
        (options.exclude ?? []).every((str) => !func.includes(str))),
    progress: (value: number) => {
      if (value >= 100 + prev.value) {
        bar.update(value);
        prev.value = value;
      }
    },
    stdio: "ignore",
  });
  bar.stop();

  // DOCUMENTATION
  try {
    await fs.promises.mkdir(`${MyConfiguration.ROOT}/docs/benchmarks`, {
      recursive: true,
    });
  } catch {}
  await fs.promises.writeFile(
    `${MyConfiguration.ROOT}/docs/benchmarks/${os
      .cpus()[0]
      .model.trim()
      .split("\\")
      .join("")
      .split("/")
      .join("")}.md`,
    DynamicBenchmarker.markdown(report),
    "utf8",
  );

  // CLOSE
  await backend.close();
};
main().catch((exp) => {
  console.error(exp);
  process.exit(-1);
});
```
  </Tabs.Tab>
  <Tabs.Tab>
```typescript filename="test/benchmark/servant.ts" showLineNumbers
import { DynamicBenchmarker } from "@nestia/benchmark";

import { MyConfiguration } from "../../src/MyConfiguration";

DynamicBenchmarker.servant({
  connection: {
    host: `http://127.0.0.1:${MyConfiguration.API_PORT()}`,
  },
  location: `${__dirname}/../features`,
  parameters: (connection) => [connection],
  prefix: "test_api_",
}).catch((exp) => {
  console.error(exp);
  process.exit(-1);
});
```
  </Tabs.Tab>
  <Tabs.Tab>
```typescript filename="test/features/api/bbs/test_api_bbs_article_create.ts" showLineNumbers
import { RandomGenerator, TestValidator } from "@nestia/e2e";
import { v4 } from "uuid";

import api from "@ORGANIZATION/PROJECT-api/lib/index";
import { IBbsArticle } from "@ORGANIZATION/PROJECT-api/lib/structures/bbs/IBbsArticle";

export async function test_api_bbs_article_create(
  connection: api.IConnection,
): Promise<void> {
  // STORE A NEW ARTICLE
  const stored: IBbsArticle = await api.functional.bbs.articles.create(
    connection,
    "general",
    {
      writer: RandomGenerator.name(),
      title: RandomGenerator.paragraph(3)(),
      body: RandomGenerator.content(8)()(),
      format: "txt",
      files: [
        {
          name: "logo",
          extension: "png",
          url: "https://somewhere.com/logo.png",
        },
      ],
      password: v4(),
    },
  );

  // READ THE DATA AGAIN
  const read: IBbsArticle = await api.functional.bbs.articles.at(
    connection,
    stored.section,
    stored.id,
  );
  TestValidator.equals("created")(stored)(read);
}
```
  </Tabs.Tab>
  <Tabs.Tab>
```typescript filename="test/features/api/bbs/test_api_bbs_article_index_search.ts" showLineNumbers
import { ArrayUtil, RandomGenerator, TestValidator } from "@nestia/e2e";

import api from "@ORGANIZATION/PROJECT-api/lib/index";
import { IBbsArticle } from "@ORGANIZATION/PROJECT-api/lib/structures/bbs/IBbsArticle";
import { IPage } from "@ORGANIZATION/PROJECT-api/lib/structures/common/IPage";

export async function test_api_bbs_article_index_search(
  connection: api.IConnection,
): Promise<void> {
  // GENERATE 100 ARTICLES
  const section: string = "general";
  const articles: IBbsArticle[] = await ArrayUtil.asyncRepeat(100)(() =>
    api.functional.bbs.articles.create(connection, section, {
      writer: RandomGenerator.name(),
      title: RandomGenerator.paragraph(4)(),
      body: RandomGenerator.content(3)()(),
      format: "txt",
      files: [],
      password: RandomGenerator.alphabets(8),
    }),
  );

  // GET ENTIRE DATA
  const total: IPage<IBbsArticle.ISummary> =
    await api.functional.bbs.articles.index(connection, section, {
      limit: articles.length,
      sort: ["-created_at"],
    });

  // PREPARE SEARCH FUNCTION
  const search = TestValidator.search("BbsArticleProvider.index()")(
    async (input: IBbsArticle.IRequest.ISearch) => {
      const page: IPage<IBbsArticle.ISummary> =
        await api.functional.bbs.articles.index(connection, section, {
          limit: articles.length,
          search: input,
          sort: ["-created_at"],
        });
      return page.data;
    },
  )(total.data, 10);

  // SEARCH TITLE
  await search({
    fields: ["title"],
    values: (article) => [article.title],
    request: ([title]) => ({ title }),
    filter: (article, [title]) => article.title.includes(title),
  });

  // SEARCH WRITER
  await search({
    fields: ["writer"],
    values: (article) => [article.writer],
    request: ([writer]) => ({ writer }),
    filter: (article, [writer]) => article.writer.includes(writer),
  });

  // SEARCH BOTH OF THEM
  await search({
    fields: ["title", "writer"],
    values: (article) => [article.title, article.writer],
    request: ([title, writer]) => ({ title, writer }),
    filter: (article, [title, writer]) =>
      article.title.includes(title) && article.writer.includes(writer),
  });
}
```
  </Tabs.Tab>
</Tabs>

You can re-use test functions for the performance benchmark program.

Just compose the benchmark's main and servant programs like above pointing the test functions' directory, then the benchmark program would utilize those test functions for the backend server performance measurement. The benchmark program will make multiple worker threads, and let them to make requests to the backend server simultaneously through the test functions.

If you want to experience the benchmark program earlier, visit below playground website.

💻 [https://stackblitz.com/~/github.com/samchon/nestia-start](https://stackblitz.com/~/github.com/samchon/nestia-start?file=README.md,docs/benchmarks/AMD%20Ryzen%209%207940HS%20w%20Radeon%20780M%20Graphics.md!,test/benchmark/index.ts,test/benchmark/servant.ts,test/features/api/bbs/test_api_bbs_article_create.ts&startScript=build:test,benchmark&view=editor)

> ```bash
> ? Number of requests to make 1024
> ? Number of threads to use 4
> ? Number of simultaneous requests to make 32
> ████████████████████████████████████████ 100% | ETA: 0s | 3654/1024
> ```
>
> [![Benchmark Image](/images/benchmark-dark.png)](https://stackblitz.com/~/github.com/samchon/nestia-start?file=README.md,docs/benchmarks/AMD%20Ryzen%209%207940HS%20w%20Radeon%20780M%20Graphics.md!,test/benchmark/index.ts,test/benchmark/servant.ts,test/features/api/bbs/test_api_bbs_article_create.ts&startScript=build:test,benchmark&view=editor)